﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace ifunction.JPush
{
    /// <summary>
    /// Class JPushWebHttpHandler.
    /// <see cref="JPushWebHttpHandler{TPushService}"/> is commonly used in web application and solutions, within supporting JSON format for request and response.
    /// When use <see cref="JPushWebHttpHandler{TPushService}"/> , you need to:
    /// 1. Create a class (e.g.: LocalJPushService in this sample) to inherit <see cref="JPushService"/> and override all abstract methods.
    /// 2. Create a class to inherit <see cref="JPush"/> and put your JPush Service (e.g.: LocalJPushService in this sample) as generic type.
    /// Following is the complete sample:
    /// <example>
    /// In Web Server:
    /// <code>
    /// namespace ifunction.JPush.Web
    ///{
    ///    public class JPush : JPushWebHttpHandler<LocalJPushService>
    ///    {
    ///        const string SecretKey = "8420D719CA8BD5FFC5C256DCE9";
    ///
    ///        public override bool Authenticate(HttpRequest request, string methodName, string postJson, NameValueCollection urlParameters, string verificationCode)
    ///        {
    ///            return (postJson.GetStringValue() + SecretKey).ToMD5(Encoding.UTF8).Equals(verificationCode.GetStringValue(), StringComparison.InvariantCultureIgnoreCase);
    ///        }
    ///    }
    ///
    ///    public class LocalJPushService : JPushService
    ///    {
    ///         // Your JPush app key
    ///        protected const string appKey = "YourAppKey";
    ///        
    ///        // Your JPush master secret
    ///        protected const string masterSecret = "YourMasterSecret";
    ///        
    ///         // Your SQL connection string
    ///         protected const string sqlConnection = "YourSqlConnectionString";
    ///        
    ///        public LocalJPushService()
    ///            : base(appKey, masterSecret, sqlConnection)
    ///        {
    ///        }
    ///
    ///        public override void ReportException(Exception exception)
    ///        {
    ///            throw exception;
    ///        }
    ///    }
    ///}
    /// </code>
    /// In Client Side, following is JSON sample of request for sending JPush Message.
    /// (Actually, it is came from <see cref="PushMessageRequestV3"/>)
    /// <code>
    /// {
    ///  "Notification": {
    ///    "android": {
    ///      "alert": "JPush V2 would be retired soon.",
    ///      "title": "JPush provides V3.",
    ///      "extras": {
    ///        "CK1": "CV1",
    ///        "CK2": "CV2"
    ///      }
    ///    },
    ///    "ios": {
    ///      "alert": "JPush V2 would be retired soon.",
    ///      "sound": "YourSound",
    ///      "badge": 1,
    ///      "extras": {
    ///        "CK1": "CV1",
    ///        "CK2": "CV2"
    ///      }
    ///    }
    ///  },
    ///  "Audience": {
    ///    "ByTagWithinAnd": [
    ///      "Tag1",
    ///      "Tag2"
    ///    ],
    ///    "ByTagWithinOr": [
    ///      "Tag3",
    ///      "Tag4"
    ///    ]
    ///  },
    ///  "AppMessage": {
    ///    "msg_content": "JPush V3 is coming.",
    ///    "title": "Test Title"
    ///  },
    ///  "Platform": 3,
    ///  "LifeTime": 86400,
    ///  "OverrideMessageId": null,
    ///  "IsTestEnvironment": true
    ///}
    /// </code>
    /// Following is sample for HTTP request
    /// <code>
    /// curl -X POST -v http://yourhost/yourHandler.ashx?method=SendPushMessage&Code=68024357B05E7B4F5341DB987DE2AABB96FC1112 -H "Content-type: application/json" -d '{"Notification":{"android":{"alert":"JPush V2 would be retired soon.","title":"JPush provides V3.","extras":{"CK1":"CV1","CK2":"CV2"}},"ios":{"alert":"JPush V2 would be retired soon.","sound":"YourSound","badge":1,"extras":{"CK1":"CV1","CK2":"CV2"}}},"Audience":{"ByTagWithinAnd":["Tag1","Tag2"],"ByTagWithinOr":["Tag3","Tag4"]},"AppMessage":{"msg_content":"JPush V3 is coming.","title":"Test Title"},"Platform":3,"LifeTime":86400,"OverrideMessageId":null,"IsTestEnvironment":true}'
    /// </code>
    /// </example>
    /// </summary>
    public abstract class JPushWebHttpHandler<TPushService> : IHttpHandler where TPushService : JPushService, new()
    {
        /// <summary>
        /// The date time converter
        /// </summary>
        protected static IsoDateTimeConverter isoDateTimeConverter = new IsoDateTimeConverter
        {
            DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffZ",
            DateTimeStyles = System.Globalization.DateTimeStyles.AdjustToUniversal
        };

        /// <summary>
        /// The service
        /// </summary>
        protected TPushService service = new TPushService();

        /// <summary>
        /// Gets a value indicating whether another request can use the <see cref="T:System.Web.IHttpHandler" /> instance.
        /// </summary>
        /// <value><c>true</c> if this instance is reusable; otherwise, <c>false</c>.</value>
        /// <returns>true if the <see cref="T:System.Web.IHttpHandler" /> instance is reusable; otherwise, false.</returns>
        public bool IsReusable
        {
            get { return false; }
        }

        /// <summary>
        /// Authenticates the specified request.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <param name="postJson">The post json.</param>
        /// <param name="urlParameters">The URL parameters.</param>
        /// <param name="verificationCode">The verification code.</param>
        /// <returns><c>true</c> if pass authentication, <c>false</c> otherwise.</returns>
        public virtual bool Authenticate(HttpRequest request, string methodName, string postJson, NameValueCollection urlParameters, string verificationCode)
        {
            return true;
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler" /> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext" /> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            object returnedObject = null;
            string methodName = null;
            string jsonParameter = null;
            string contentType = "application/json";
            string verificationHash = null;

            try
            {
                var parameters = HttpUtility.ParseQueryString(context.Request.Url.Query);

                if (parameters != null)
                {
                    methodName = parameters["method"];
                    verificationHash = parameters["Code"];
                }

                if (!string.IsNullOrWhiteSpace(methodName))
                {
                    object parameterObject = null;
                    jsonParameter = GetJsonParameter(context.Request);

                    if (!Authenticate(context.Request, methodName, jsonParameter, parameters, verificationHash))
                    {
                        throw new UnauthorizedAccessException("Failed to authenticate for URL: " + context.Request.Url.ToString());
                    }

                    var reflectedMethodInfo = typeof(TPushService).GetMethod(methodName);

                    if (reflectedMethodInfo == null || !reflectedMethodInfo.IsPublic || reflectedMethodInfo.IsStatic)
                    {
                        throw new InvalidOperationException("Method is not found or can not be invoked. Only public & non-static method can be invoke.");
                    }

                    var inputParameters = reflectedMethodInfo.GetParameters();

                    if (inputParameters != null && inputParameters.Count() > 0)
                    {
                        var inputType = inputParameters[0].ParameterType;

                        parameterObject = JsonConvert.DeserializeObject(jsonParameter, inputType);
                    }

                    returnedObject = reflectedMethodInfo.Invoke(service, new object[] { parameterObject });
                }
            }
            catch (Exception ex)
            {
                if (context.Request.IsLocal)
                {
                    returnedObject = new
                                    {
                                        Message = ex.Message,
                                        ex.Source,
                                        ex.StackTrace,
                                        ex.TargetSite,
                                        RequestUrl = context.Request.Url.ToString(),
                                        RequestData = jsonParameter
                                    };
                }
                else
                {
                    returnedObject = new
                    {
                        Message = "Failed to invoke specific method. Please check error message at server, or use local debug to see more information. ",
                        RequestUrl = context.Request.Url.ToString(),
                        RequestData = jsonParameter
                    };
                }

                service.ReportException(ex);
            }

            context.Response.ContentType = contentType;
            context.Response.Write(JsonConvert.SerializeObject(returnedObject, isoDateTimeConverter));
        }

        #region Protected methods

        /// <summary>
        /// Gets the json parameter.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns>System.String.</returns>
        /// <exception cref="System.InvalidOperationException">Failed to get JSON parameter from HTTP request.</exception>
        protected static string GetJsonParameter(HttpRequest request)
        {
            string jsonParameter = null;

            try
            {
                var bytes = GetPostDataFromHttpWebRequest(request);
                jsonParameter = Encoding.UTF8.GetString(bytes);
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("Failed to get JSON parameter from HTTP request.", ex);
            }

            return jsonParameter;
        }

        /// <summary>
        /// Gets the post data from HTTP web request.
        /// </summary>
        /// <param name="httpRequest">The HTTP request.</param>
        /// <returns>System.Byte[].</returns>
        protected static byte[] GetPostDataFromHttpWebRequest(HttpRequest httpRequest)
        {
            byte[] data = null;

            if (httpRequest != null)
            {
                MemoryStream ms = new MemoryStream();
                httpRequest.InputStream.CopyTo(ms);
                data = ms.ToArray();
            }

            return data;
        }

        #endregion
    }
}
